/* KeyGenerator.m created by cfeder on Wed 26-Oct-1994 */

#import "KeyGenerator.h"
#import "ValueForKey.h"

// Default implementation for all EOs to generate their own primary keys
@implementation NSObject (assignPrimaryKey)
- (void)assignPrimaryKeyForEntity:(EOEntity *)entity
{
    NSString *primaryKeyName = [[entity primaryKeyAttributeNames] objectAtIndex:0];
    NSNumber *value = [NSNumber numberWithUnsignedInt:[KeyGenerator nexKeyForEntity:entity]];
    NSDictionary *pkDict = [NSDictionary dictionaryWithObjects:&value forKeys:&primaryKeyName count:1];
    [self takeValuesFromDictionary:pkDict];
}

- (void)assignPrimaryKeyIfNotAlreadyPresentForEntity:(EOEntity *)entity
{
    // Figure out whether we already have a key assigned
    BOOL hasKey = NO;
    NSEnumerator *keys = [[entity primaryKeyAttributeNames] objectEnumerator];
    NSString *key;
    while (key = [keys nextObject]) {
        id value = [self valueForKey:key];
        if (value && (value != [EONull null]) && (![value isKindOfClass:[NSNumber class]] || ([value intValue]!=0))){
            hasKey = YES;
            break;
        }
    }

    // if we don't already have a primary key, then assign ourselves one
    // get our current primary key values
    if (!hasKey) {
        [self assignPrimaryKeyForEntity:entity];
    }
}
@end

// Assumptions:
// Model contains:
// - login information
// - UniqueKey entity with attributes entity_name, and max_key


@implementation KeyGenerator
#define RESERVE_SET_SIZE 5


- initWithEntity:(EOEntity *)anEntity dataSource:(EODatabaseDataSource *)aDataSource
{
    [super init];
    entity = [anEntity retain];
    dataSource = [aDataSource retain];
    lastGranted = lastReserved = 0;
    return self;
}

- (unsigned)nextKey
{
    if (lastGranted >= lastReserved) {
        // Go out to database and reserve a block of keys
        EOQualifier	*tableNameQualifier;
        EODatabaseChannel *channel = [dataSource databaseChannel];
        EODatabaseContext *context = [channel databaseContext];
        NSString *externalName = [entity externalName];
        EOEntity *uniqueKeyEntity = [dataSource entity];

        if (!uniqueKeyEntity)
            [NSException raise:NSInternalInconsistencyException format:@"%s: model does not contain UniqueKey entity '%@'", sel_getName(_cmd), [entity name]];

        if (![channel isOpen])
            [channel openChannel];
        [context beginTransaction];

        // This is the first time for this entity.  We need to get the current setting.
        if (!currentMaxRecord) {
            // Construct the qualifier for our name
            tableNameQualifier = [[[EOQualifier alloc] initWithEntity:uniqueKeyEntity
                                   qualifierFormat:@"%A= '%@'", @"entity_name", externalName] autorelease];
            [channel selectObjectsDescribedByQualifier:tableNameQualifier fetchOrder:nil];
            currentMaxRecord = [[channel fetchWithZone:[self zone]] retain];
            [channel cancelFetch];
        }

        if (currentMaxRecord) {
            // attempt to update the max_key value
            for(;;) {
                lastGranted = [[currentMaxRecord objectForKey:@"max_key"] unsignedIntValue];
                lastReserved = lastGranted + RESERVE_SET_SIZE;
                [currentMaxRecord setObject:[NSNumber numberWithUnsignedInt:lastReserved] forKey:@"max_key"];

                if ([channel updateObject:currentMaxRecord] && [context commitTransaction])
                    break;  // success!!

                // Update failed.  Setup for another pass
                [context rollbackTransaction];
                [context beginTransaction];
                [channel refetchObject:currentMaxRecord];
            }
        } else {
            // we have to insert a record for this entity
            currentMaxRecord = [[EOGenericRecord alloc] initWithPrimaryKey:nil entity:uniqueKeyEntity];  // retained
            [currentMaxRecord setObject:externalName forKey:@"entity_name"];
            lastReserved = RESERVE_SET_SIZE;
            lastGranted = 0;
            [currentMaxRecord setObject:[NSNumber numberWithUnsignedInt:lastReserved] forKey:@"max_key"];
            NSLog(@"Entity '%@' not in UniqueKey table.  Adding record: %@", [entity name], currentMaxRecord);
            if (![channel insertObject:currentMaxRecord])
                [NSException raise:NSInternalInconsistencyException format:@"Failed to insert UniqueKey object for entity: %@", [entity name]];
            [context commitTransaction];
        }
    }

    lastGranted++;        
    return lastGranted;
}

- (void)dealloc
{
    [entity release];
    [dataSource release];
    [super dealloc];
}


//
// Class methods to automatically cache instances
//
static NSMutableDictionary *entityNameToGenerator;

+ (EODatabaseDataSource *)dataSourceForModel:(EOModel *)model
{
    // use the datasource rendevous mechanism to do our caching for us
    return [[EODatabaseDataSource alloc] initWithModelName:[model name]
                                                entityName:@"unique_key"
                                              databaseName:nil
                                               contextName:@"_UniqueKey"
                                               channelName:nil];
}

+ keyGeneratorForEntity:(EOEntity *)anEntity
{
    KeyGenerator *kg = [entityNameToGenerator objectForKey:[anEntity name]];
    if (!kg) {
        EODatabaseDataSource *ds = [self dataSourceForModel:[anEntity model]];
        if (!ds)
            [NSException raise:NSInternalInconsistencyException format:@"%s: unable to allocation UniqueKey data source for entity '%@'", sel_getName(_cmd), [anEntity name]];
        kg = [[KeyGenerator alloc] initWithEntity:anEntity dataSource:ds];

        if (kg) {
            if (!entityNameToGenerator)
                entityNameToGenerator = [NSMutableDictionary new];
            [entityNameToGenerator setObject:kg forKey:[anEntity name]];
        }
    }
    return kg;
}

+ (unsigned)nexKeyForEntity:(EOEntity *)anEntity
{
    KeyGenerator *kg = [self keyGeneratorForEntity:anEntity];
    if (!kg)
        [NSException raise:NSInternalInconsistencyException format:@"%s: unable to create KeyGenerator for entity '%@'", sel_getName(_cmd), [anEntity name]];
    return [kg nextKey]; 
}

@end
